package top.rainbowecho.gateway.security.authentication.open;

import cn.hutool.core.io.IoUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;
import top.rainbowecho.common.util.ExceptionMessageContent;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

import static org.apache.commons.lang3.StringUtils.isEmpty;

/**
 * 此过滤器只接收content-type为application/json或application/json;charset=UTF-8的请求
 *
 * @author rainbow
 * @since 2019/12/20 18:51
 */
@Slf4j
public class OpenIdAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    private static final String SECURITY_FORM_OPEN_ID_KEY = "openId";
    private static final String SECURITY_FORM_PROVIDER_ID_KEY = "providerId";
    private static final String SECURITY_FORM_USER_INFO_KEY = "userInfo";

    private String openIdParameter = SECURITY_FORM_OPEN_ID_KEY;
    private String providerParameter = SECURITY_FORM_PROVIDER_ID_KEY;
    private String userInfoParameter = SECURITY_FORM_USER_INFO_KEY;

    private boolean postOnly = true;

    // ~ Constructors
    // ===================================================================================================

    public OpenIdAuthenticationFilter(String processUrl) {
        super(new AntPathRequestMatcher(processUrl, "POST"));
    }

    // ~ Methods
    // ========================================================================================================

    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !HttpMethod.POST.matches(request.getMethod())) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        OpenIdRequest openIdRequest;
        try {
            String requestBody = IoUtil.read(request.getInputStream(), StandardCharsets.UTF_8);

            log.info("request body: " + requestBody);
            if (!StringUtils.equals(MediaType.APPLICATION_JSON_VALUE, request.getContentType())
                    && !StringUtils.equals(MediaType.APPLICATION_JSON_UTF8_VALUE, request.getContentType())) {
                throw new OpenIdAuthenticationException("不支持此content-type");
            }
            ObjectMapper objectMapper = new ObjectMapper();
            openIdRequest = objectMapper.readValue(requestBody, OpenIdRequest.class);
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new AuthenticationServiceException(e.getMessage());
        }

        String openId = openIdRequest.getOpenId();
        log.info("openId param: " + openId);
        String providerId = openIdRequest.getProviderId();
        log.info("providerId param: " + providerId);
        Object userInfo = openIdRequest.getUserInfo();
        log.info("userInfo param: " + userInfo);

        if (isEmpty(openId) || isEmpty(providerId) || userInfo == null) {
            throw new OpenIdAuthenticationException(ExceptionMessageContent.MISSING_ARG);
        }

        openId = openId.trim();

        OpenIdAuthenticationToken authRequest = new OpenIdAuthenticationToken(openId, providerId, userInfo);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }

    /**
     * 从请求中获取第三方用户信息
     *
     * @param request request请求
     * @return 第三方用户信息
     */
    protected Object obtainUserInfo(HttpServletRequest request) {
        return request.getParameter(userInfoParameter);
    }

    /**
     * Enables subclasses to override the composition of the password, such as by
     * including additional values and a separator.
     * <p>
     * This might be used for example if a postcode/zipcode was required in addition to
     * the password. A delimiter such as a pipe (|) should be used to separate the
     * password and extended value(s). The <code>AuthenticationDao</code> will need to
     * generate the expected password in a corresponding manner.
     * </p>
     *
     * @param request so that request attributes can be retrieved
     *
     * @return the password that will be presented in the <code>Authentication</code>
     * request token to the <code>AuthenticationManager</code>
     */
    protected String obtainProviderId(HttpServletRequest request) {
        return request.getParameter(providerParameter);
    }

    /**
     * Enables subclasses to override the composition of the username, such as by
     * including additional values and a separator.
     *
     * @param request so that request attributes can be retrieved
     *
     * @return the username that will be presented in the <code>Authentication</code>
     * request token to the <code>AuthenticationManager</code>
     */
    protected String obtainOpenId(HttpServletRequest request) {
        return request.getParameter(openIdParameter);
    }

    /**
     * Provided so that subclasses may configure what is put into the authentication
     * request's details property.
     *
     * @param request that an authentication request is being created for
     * @param authRequest the authentication request object that should have its details
     * set
     */
    protected void setDetails(HttpServletRequest request,
                              OpenIdAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    /**
     * Sets the parameter name which will be used to obtain the username from the login
     * request.
     *
     * @param openIdParameter the parameter name. Defaults to "username".
     */
    public void setOpenIdParameter(String openIdParameter) {
        Assert.hasText(openIdParameter, "OpenId parameter must not be empty or null");
        this.openIdParameter = openIdParameter;
    }

    /**
     * Sets the parameter name which will be used to obtain the password from the login
     * request..
     *
     * @param providerParameter the parameter name. Defaults to "password".
     */
    public void setProviderParameter(String providerParameter) {
        Assert.hasText(providerParameter, "ProviderId parameter must not be empty or null");
        this.providerParameter = providerParameter;
    }

    public void setUserInfoParameter(String userInfoParameter) {
        Assert.hasText(userInfoParameter, "UserInfo parameter must not be empty or null");
        this.userInfoParameter = userInfoParameter;
    }

    /**
     * Defines whether only HTTP POST requests will be allowed by this filter. If set to
     * true, and an authentication request is received which is not a POST request, an
     * exception will be raised immediately and authentication will not be attempted. The
     * <tt>unsuccessfulAuthentication()</tt> method will be called as if handling a failed
     * authentication.
     * <p>
     * Defaults to <tt>true</tt> but may be overridden by subclasses.
     */
    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getOpenIdParameter() {
        return openIdParameter;
    }

    public final String getPasswordParameter() {
        return providerParameter;
    }
}
